This script performs preprocessing and quality control (QC) filtering
of single-cell RNA sequencing (scRNA-seq) data generated using
conventional 10x Genomics, FLEX, and Parse Biosciences Mini and Midi
kits. The R package CRMetrics is utilized for QC in this script. While
the published version of CRMetrics (https://github.com/khodosevichlab/CRMetrics) is only
compatible with conventional 10x Genomics data, a local adaptation has
been implemented to support 10x FLEX and Parse Biosciences data.
Additionally, this version incorporates compatibility with the latest
CellBender release (version 0.3.0). For large datasets (~ 100,000
nuclei) memory limitations of R (e.g. with Conos and CRMetrics)
necessitate transitioning to Python for data analysis. More information
and application of the CRMetrics package can be found here: http://kkh.bric.ku.dk/rasmus/CRMetrics/walkthrough.html
QC filtering involves iterative adjustments to determine the optimal
thresholds for filtering depth (unique molecular identifiers [UMIs] per
cell), mitochondrial (mito) fraction (percentage of mitochondrial gene
expression per cell), and doublets (two or more cells clustered
together). Final clustering is not conducted in this script, it is
addressed in a subsequent script, “Clustering_and_annotation”.
If ambient RNA filtering is applied, it is performed first, followed
by filtering based on depth, mitochondrial fraction, and doublets. At
the end of the pipeline, a new CRMetrics object is constructed using the
filtered count matrices (=cms) to plot the violin plots for UMI and gene
counts post-filtering.
Due to its computational intensity, ambient RNA filtering is not part
of the standard analysis pipeline but is performed upon specific user
request.
Setup
library(devtools)
# loading locally adapted CRMetrics version
#load_all("/people/nrq364/CRMetrics_LW")
library(CRMetrics)
library(magrittr)
library(dplyr)
library(conos)
library(pagoda2)
library(qs)
1. fastQC and multiQC
Provide links to multiQC reports of raw sequencing data and
cellranger metrics. Does not yet work on cellranger multi (FLEX) and
Parse output.
2. Create CRMetrics object
In case of Parse data use data.path to “combined” folder, for 10x
FLEX data.path to “per_sample_outs” folder. Metadata should at least
contain “sample” and “group/condition” but can have more columns.
crm <- CRMetrics$new(data.path = "/data/counts/[project_name]/",
metadata = "path_to_metadata.csv",
n.cores = 60)
crm$metadata
qsave(crm, "crm.qs", nthreads = 10)
3. Basic metrics
Show available metrics to plot.
crm$selectMetrics()
Plot selected metrics per sample
e.g. cells per sample, median genes, median UMIs, mean reads
metrics.to.plot <- crm$selectMetrics(ids = c(1:3,5,6))
crm$plotSummaryMetrics(plot.geom = "bar", comp.group = "sample", metrics = metrics.to.plot)

Plot selected metrics per condition
We can do the same, but set the comparison group to condition. This
will add statistics to the plots. Additionaly, we can add a second
comparison group for coloring.
crm$plotSummaryMetrics(comp.group = "condition",
metrics = metrics.to.plot,
plot.geom = "point",
stat.test = "non-parametric") #second.comp.group= "sex"

Add and pot detailed metrices
crm$addCms(add.metadata=F) #for Parse data: parse = T, for FLEX: flex = T
crm$addDetailedMetrics(n.cores = 30)
get total number of sequenced nuclei
sum(sapply(crm$cms, function(d) dim(d)[2]))
[1] 61975
plot UMIs and genes per sample before filtering, horizontal lines
indicate the median values for all samples
crm$plotDetailedMetrics(comp.group = "condition",
metrics = c("UMI_count", "gene_count"),
plot.geom = "violin")

Generate a data frame with the metrics.
metrics <- crm$summary.metrics
metrics %<>% as.data.frame()
metrics
Get median genes per cell across all samples before filtering (in
case of Parse: “hg38_median_genes_per_cell”).
median(metrics[metrics$metric=="median genes per cell",]$value)
[1] 3478
Get median UMIs per cell across all samples before filtering (in case
of Parse: “hg38_median_tscp_per_cell”).
median(metrics[metrics$metric=="median umi counts per cell",]$value)
[1] 7945
If ambient RNA removal is part of the pipeline, it is performed here.
Relevant code is located further down in the script under 8.
4. Embed cells using conos
To plot the cells in an embedding, we need to preprocess the raw
count matrices with pagoda2 (seurat is also possible).
crm$doPreprocessing() #min.transcripts.per.cell = 100
Create embedding using conos. This functions executes the functions
Conos$new(cms, n.cores = n.cores), buildGraph(), findCommunities(),
embedGraph(method = “UMAP”), with default settings, which can be
adjusted.
crm$createEmbedding()
Plot the embedding. This is before filtering, so we are not yet
interested in the clusters, those are refined in the next script.
crm$plotEmbedding()

5. Filtering out low quality cells
Cell depth
Plot cell depth in the embedding or as histograms per sample.
crm$plotEmbedding(depth = TRUE,
depth.cutoff = 1000)

crm$plotDepth()

If the depth distribution varies between samples, we can use
sample-specific cutoffs.
depth_cutoff_vec <- c(1e3, 1e3, 500, 1e3, 1e3, 500, 1e3, 1e3, 1e3, 1e3, 1e3, 500) %>%
setNames(crm$detailed.metrics$sample %>%
unique() %>%
sort())
depth_cutoff_vec
control_1 control_2 control_4 control_5 control_6 control_7 treated_1 treated_2 treated_3 treated_4
1000 1000 500 1000 1000 500 1000 1000 1000 1000
treated_6 treated_7
1000 500
Plot sample specific cutoff in histogram and embedding.
crm$plotDepth(cutoff = depth_cutoff_vec)

crm$plotEmbedding(depth = TRUE,
depth.cutoff = depth_cutoff_vec)

Mitochondrial gene fraction
Investigate the mitochondrial gene fraction in the cells. The example
dataset has a very low expression of mitochondrial genes. Usual cutoff
is 5% or higher.
crm$plotEmbedding(mito.frac = TRUE,
mito.cutoff = 0.01,
species = "mouse")

We can plot the distribution of the mitochondrial fraction per sample
and also include sample-wise cutoffs (not shown here as the example
dataset has only a very low expression of mitochondrial genes).
Doublet detection
Doublet detection within CRMetrics is possible with the python
packages scrublet or DoubletDetection Scrublet is the default method,
which is fast. DoubletDetection is significantly slower, but performs
better according to this review (https://www.sciencedirect.com/science/article/pii/S2405471220304592).
Whcih one to choose depends on the dataset and technology used.
Here we use scrublet as default method. A virtual environment is used
to install scrublet.
library(reticulate)
conda_create("scrublet",
conda = "/opt/software/miniconda/4.12.0/condabin/conda",
python_version = 3.8)
Run scrublet from within R or generate a Python script to run it from
the terminal. To do this, set export = TRUE and run python scrublet.py
inside the virtual environment to generate data.
crm$detectDoublets(env = "scrublet",
conda.path = "/opt/software/miniconda/4.12.0/condabin/conda",
method = "scrublet")
Then, load data with:
crm$addDoublets()
Plot the estimated doublets in the embedding.
crm$plotEmbedding(doublet.method = "scrublet")

Plot filtered cells
Plot all cells to be filtered on the embedding.
crm$plotFilteredCells(type = "embedding",
depth = TRUE,
depth.cutoff = 1000,
doublet.method = "scrublet",
mito.frac = FALSE,
species = "mouse")
Scale for colour is already present.
Adding another scale for colour, which will replace the existing scale.

Plot the cells to be filtered per sample in a bar plot where
combination means a cell that has at least two filter labels,
e.g. doublet and depth.
crm$plotFilteredCells(type = "bar",
depth = TRUE,
depth.cutoff = 1000,
doublet.method = "scrublet",
mito.frac = FALSE,
species = "mouse")

6. Filter and save count matrices
Finally, filter the count matrices to create a cleaned list to be
used in downstream applications. If cellbender should be used then cms
needs to be added after cellbender and then all subsequent filtering is
done.
crm$filterCms(depth.cutoff = 1000,
mito.cutoff = NULL,
doublets = "scrublet",
samples.to.exclude = NULL,
species = "mouse",
verbose = TRUE)
The filtered list of count matrices is stored in $cms.filtered which
can be saved on disk.
qsave(crm$cms.filtered, "cms_filtered[_cellbender].qs",
nthreads = 10)
7. Plot detailed metrics after filtering
cms <- qread("cms_filtered[_cellbender].qs")
Create a new crm object with cms after filtering.
crm_filtered <- CRMetrics$new(cms = crm$cms.filtered, n.cores = 60, metadata="path_to_metadata.csv")
crm_filtered$addSummaryFromCms()
crm_filtered$addDetailedMetrics(n.cores = 30)
Total number of nuclei retained after QC:
sum(sapply(crm_filtered$cms, function(d) dim(d)[2]))
Plot UMIs and genes per sample after QC filtering, horizontal lines
indicate the median values for all samples.
crm_filtered$plotDetailedMetrics(comp.group = "condition",
metrics = c("UMI_count", "gene_count"),
plot.geom = "violin")
Generate a data frame with the metrics.
metrics <- crm_filtered$summary.metrics
metrics %<>% as.data.frame()
metrics
Median genes per cell across all samples after QC filtering (in case
of Parse: “hg38_median_genes_per_cell”).
median(metrics[metrics$metric=="median genes per cell",]$value)
Median UMIs per cell across all samples after QC filtering (in cas of
Parse: “hg38_median_tscp_per_cell”).
median(metrics[metrics$metric=="median umi counts per cell",]$value)
8. Optional: remove ambient RNA
We use usually cellbender, SoupX is also included in CRMetrics.
Perform this step before filtering, it changes the number of called
cells and their gene counts.
In the old cellbender version (before 0.3.0) we needed to specify
expected number of cells and total droplets included. As hinted in the
manual, the number of total droplets included could be expected number
of cells multiplied by 3, this can still be plotted here when setting
show.total.droplets=T. Additionally, in case of Parse, the following
function creates the input files for cellbender from the Parse
output.
crm$prepareCellbender(show.expected.cells = T, show.total.droplets = T)
2025-04-30 10:32:28.050553 Started run using 12 cores
2025-04-30 10:32:28.057862 Using stored HDF5 Cell Ranger/Parse outputs. To overwrite, set $cms.raw <- NULL
2025-04-30 10:32:28.058687 Using stored UMI counts calculations. To overwrite, set $cellbender$umi.counts <- NULL
2025-04-30 10:32:28.059399 Plotting
2025-04-30 10:32:28.885673 Done!

The following function prepares and saves the cellbender script. To
only select specific samples, use the samples parameter. total.droplets
and expected.cells is a logical because in cellbender from v0.3.0
total.droplets and expected.cells do not need to be specified anymore.
○ FALSE is default
○ TRUE would take expected.cells from
metadata and total droplets would be 3x expected cells
○
alternatively give own numbers (named vector)
crm$saveCellbenderScript(file = "cellbender_script.sh",
fpr = 0.01,
epochs = 150,
use.gpu = TRUE, total.droplets = F, expected.cells = F) # could be total.droplets = droplets (named vector)
We can run this script in the terminal (screen session). Here, we
activate the environment: conda activate cellbender_v0.3.0 and we run
the bash script: sh cellbender_script.sh.
Plot cellbender results
Plot the changes in cell numbers following CellBender
estimations.
crm$plotCbCells()

Plot the CellBender training results.
crm$plotCbTraining()

Plot the cell probabilities.
crm$plotCbCellProbs()

Plot the identified ambient genes per sample.
crm$plotCbAmbExp(cutoff = 0.005)

Lastly, plot the proportion of samples expressing ambient genes.
MALAT1 is identified as an ambient gene in almost all samples which is
expected.
crm$plotCbAmbGenes(cutoff = 0.005)

Add cellbender filtered cms
Then, we can add the cellbender filtered cms to our object.
Additionally, it is recommended to generate new summary metrics from the
adjusted cms which can then be plotted.
crm$cms <- NULL
crm$addCms(cellbender = T)
crm$addSummaryFromCms()
crm$detailed.metrics <- NULL
crm$addDetailedMetrics()
qsave(crm, "crm_cellbender.qs")
LS0tCnRpdGxlOiAiMSkgUHJlcHJvY2Vzc2luZyBhbmQgUUMgZmlsdGVyaW5nIgpkYXRlOiAiYHIgU3lzLkRhdGUoKWAiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB0cnVlCiAgICB0b2NfZmxvYXQ6IHRydWUKICAgIGNvZGVfZm9sZGluZzogbm9uZQotLS0KClRoaXMgc2NyaXB0IHBlcmZvcm1zIHByZXByb2Nlc3NpbmcgYW5kIHF1YWxpdHkgY29udHJvbCAoUUMpIGZpbHRlcmluZyBvZiBzaW5nbGUtY2VsbCBSTkEgc2VxdWVuY2luZyAoc2NSTkEtc2VxKSBkYXRhIGdlbmVyYXRlZCB1c2luZyBjb252ZW50aW9uYWwgMTB4IEdlbm9taWNzLCBGTEVYLCBhbmQgUGFyc2UgQmlvc2NpZW5jZXMgTWluaSBhbmQgTWlkaSBraXRzLgpUaGUgUiBwYWNrYWdlIENSTWV0cmljcyBpcyB1dGlsaXplZCBmb3IgUUMgaW4gdGhpcyBzY3JpcHQuIFdoaWxlIHRoZSBwdWJsaXNoZWQgdmVyc2lvbiBvZiBDUk1ldHJpY3MgKGh0dHBzOi8vZ2l0aHViLmNvbS9raG9kb3NldmljaGxhYi9DUk1ldHJpY3MpIGlzIG9ubHkgY29tcGF0aWJsZSB3aXRoIGNvbnZlbnRpb25hbCAxMHggR2Vub21pY3MgZGF0YSwgYSBsb2NhbCBhZGFwdGF0aW9uIGhhcyBiZWVuIGltcGxlbWVudGVkIHRvIHN1cHBvcnQgMTB4IEZMRVggYW5kIFBhcnNlIEJpb3NjaWVuY2VzIGRhdGEuIEFkZGl0aW9uYWxseSwgdGhpcyB2ZXJzaW9uIGluY29ycG9yYXRlcyBjb21wYXRpYmlsaXR5IHdpdGggdGhlIGxhdGVzdCBDZWxsQmVuZGVyIHJlbGVhc2UgKHZlcnNpb24gMC4zLjApLiBGb3IgbGFyZ2UgZGF0YXNldHMgKH4gMTAwLDAwMCBudWNsZWkpIG1lbW9yeSBsaW1pdGF0aW9ucyBvZiBSIChlLmcuIHdpdGggQ29ub3MgYW5kIENSTWV0cmljcykgbmVjZXNzaXRhdGUgdHJhbnNpdGlvbmluZyB0byBQeXRob24gZm9yIGRhdGEgYW5hbHlzaXMuIE1vcmUgaW5mb3JtYXRpb24gYW5kIGFwcGxpY2F0aW9uIG9mIHRoZSBDUk1ldHJpY3MgcGFja2FnZSBjYW4gYmUgZm91bmQgaGVyZTogaHR0cDovL2traC5icmljLmt1LmRrL3Jhc211cy9DUk1ldHJpY3Mvd2Fsa3Rocm91Z2guaHRtbAoKUUMgZmlsdGVyaW5nIGludm9sdmVzIGl0ZXJhdGl2ZSBhZGp1c3RtZW50cyB0byBkZXRlcm1pbmUgdGhlIG9wdGltYWwgdGhyZXNob2xkcyBmb3IgZmlsdGVyaW5nIGRlcHRoICh1bmlxdWUgbW9sZWN1bGFyIGlkZW50aWZpZXJzIFtVTUlzXSBwZXIgY2VsbCksIG1pdG9jaG9uZHJpYWwgKG1pdG8pIGZyYWN0aW9uIChwZXJjZW50YWdlIG9mIG1pdG9jaG9uZHJpYWwgZ2VuZSBleHByZXNzaW9uIHBlciBjZWxsKSwgYW5kIGRvdWJsZXRzICh0d28gb3IgbW9yZSBjZWxscyBjbHVzdGVyZWQgdG9nZXRoZXIpLiBGaW5hbCBjbHVzdGVyaW5nIGlzIG5vdCBjb25kdWN0ZWQgaW4gdGhpcyBzY3JpcHQsIGl0IGlzIGFkZHJlc3NlZCBpbiBhIHN1YnNlcXVlbnQgc2NyaXB0LCAiQ2x1c3RlcmluZ19hbmRfYW5ub3RhdGlvbiIuCgpJZiBhbWJpZW50IFJOQSBmaWx0ZXJpbmcgaXMgYXBwbGllZCwgaXQgaXMgcGVyZm9ybWVkIGZpcnN0LCBmb2xsb3dlZCBieSBmaWx0ZXJpbmcgYmFzZWQgb24gZGVwdGgsIG1pdG9jaG9uZHJpYWwgZnJhY3Rpb24sIGFuZCBkb3VibGV0cy4gQXQgdGhlIGVuZCBvZiB0aGUgcGlwZWxpbmUsIGEgbmV3IENSTWV0cmljcyBvYmplY3QgaXMgY29uc3RydWN0ZWQgdXNpbmcgdGhlIGZpbHRlcmVkIGNvdW50IG1hdHJpY2VzICg9Y21zKSB0byBwbG90IHRoZSB2aW9saW4gcGxvdHMgZm9yIFVNSSBhbmQgZ2VuZSBjb3VudHMgcG9zdC1maWx0ZXJpbmcuCgpEdWUgdG8gaXRzIGNvbXB1dGF0aW9uYWwgaW50ZW5zaXR5LCBhbWJpZW50IFJOQSBmaWx0ZXJpbmcgaXMgbm90IHBhcnQgb2YgdGhlIHN0YW5kYXJkIGFuYWx5c2lzIHBpcGVsaW5lIGJ1dCBpcyBwZXJmb3JtZWQgdXBvbiBzcGVjaWZpYyB1c2VyIHJlcXVlc3QuCgoKIyBTZXR1cApgYGB7ciwgbWVzc2FnZT1GfQpsaWJyYXJ5KGRldnRvb2xzKQojIGxvYWRpbmcgbG9jYWxseSBhZGFwdGVkIENSTWV0cmljcyB2ZXJzaW9uCiNsb2FkX2FsbCgiL3Blb3BsZS9ucnEzNjQvQ1JNZXRyaWNzX0xXIikKbGlicmFyeShDUk1ldHJpY3MpCmxpYnJhcnkobWFncml0dHIpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkoY29ub3MpCmxpYnJhcnkocGFnb2RhMikKbGlicmFyeShxcykKYGBgCiMgMS4gZmFzdFFDIGFuZCBtdWx0aVFDClByb3ZpZGUgbGlua3MgdG8gbXVsdGlRQyByZXBvcnRzIG9mIHJhdyBzZXF1ZW5jaW5nIGRhdGEgYW5kIGNlbGxyYW5nZXIgbWV0cmljcy4gRG9lcyBub3QgeWV0IHdvcmsgb24gY2VsbHJhbmdlciBtdWx0aSAoRkxFWCkgYW5kIFBhcnNlIG91dHB1dC4KCiMgMi4gQ3JlYXRlIENSTWV0cmljcyBvYmplY3QKSW4gY2FzZSBvZiBQYXJzZSBkYXRhIHVzZSBkYXRhLnBhdGggdG8gImNvbWJpbmVkIiBmb2xkZXIsIGZvciAxMHggRkxFWCBkYXRhLnBhdGggdG8gInBlcl9zYW1wbGVfb3V0cyIgZm9sZGVyLiAKTWV0YWRhdGEgc2hvdWxkIGF0IGxlYXN0IGNvbnRhaW4gInNhbXBsZSIgYW5kICJncm91cC9jb25kaXRpb24iIGJ1dCBjYW4gaGF2ZSBtb3JlIGNvbHVtbnMuCmBgYHtyfQpjcm0gPC0gQ1JNZXRyaWNzJG5ldyhkYXRhLnBhdGggPSAiL2RhdGEvY291bnRzL1twcm9qZWN0X25hbWVdLyIsIAogICAgICAgICAgICAgICAgICAgICBtZXRhZGF0YSA9ICJwYXRoX3RvX21ldGFkYXRhLmNzdiIsIAogICAgICAgICAgICAgICAgICAgICBuLmNvcmVzID0gNjApCmBgYAoKYGBge3J9CmNybSRtZXRhZGF0YQpgYGAKCmBgYHtyfQpxc2F2ZShjcm0sICJjcm0ucXMiLCBudGhyZWFkcyA9IDEwKQpgYGAKCiMgMy4gQmFzaWMgbWV0cmljcwoKU2hvdyBhdmFpbGFibGUgbWV0cmljcyB0byBwbG90LgpgYGB7cn0KY3JtJHNlbGVjdE1ldHJpY3MoKQpgYGAKCiMjIFBsb3Qgc2VsZWN0ZWQgbWV0cmljcyBwZXIgc2FtcGxlCmUuZy4gY2VsbHMgcGVyIHNhbXBsZSwgbWVkaWFuIGdlbmVzLCBtZWRpYW4gVU1JcywgbWVhbiByZWFkcwpgYGB7ciwgZmlnLmhlaWdodD02LCB3YXJuaW5nPUZ9Cm1ldHJpY3MudG8ucGxvdCA8LSBjcm0kc2VsZWN0TWV0cmljcyhpZHMgPSBjKDE6Myw1LDYpKQpjcm0kcGxvdFN1bW1hcnlNZXRyaWNzKHBsb3QuZ2VvbSA9ICJiYXIiLCBjb21wLmdyb3VwID0gInNhbXBsZSIsIG1ldHJpY3MgPSBtZXRyaWNzLnRvLnBsb3QpCmBgYAoKIyMgUGxvdCBzZWxlY3RlZCBtZXRyaWNzIHBlciBjb25kaXRpb24KV2UgY2FuIGRvIHRoZSBzYW1lLCBidXQgc2V0IHRoZSBjb21wYXJpc29uIGdyb3VwIHRvIGNvbmRpdGlvbi4gVGhpcyB3aWxsIGFkZCBzdGF0aXN0aWNzIHRvIHRoZSBwbG90cy4gQWRkaXRpb25hbHksIHdlIGNhbiBhZGQgYSBzZWNvbmQgY29tcGFyaXNvbiBncm91cCBmb3IgY29sb3JpbmcuCmBgYHtyLCBmaWcuaGVpZ2h0PTYsIHdhcm5pbmc9Rn0KY3JtJHBsb3RTdW1tYXJ5TWV0cmljcyhjb21wLmdyb3VwID0gImNvbmRpdGlvbiIsCiAgICAgICAgICAgICAgICAgICAgICAgbWV0cmljcyA9IG1ldHJpY3MudG8ucGxvdCwgCiAgICAgICAgICAgICAgICAgICAgICAgcGxvdC5nZW9tID0gInBvaW50IiwgCiAgICAgICAgICAgICAgICAgICAgICAgc3RhdC50ZXN0ID0gIm5vbi1wYXJhbWV0cmljIikgI3NlY29uZC5jb21wLmdyb3VwPSAic2V4IgpgYGAKCiMjIEFkZCBhbmQgcG90IGRldGFpbGVkIG1ldHJpY2VzCmBgYHtyfQpjcm0kYWRkQ21zKGFkZC5tZXRhZGF0YT1GKSAjZm9yIFBhcnNlIGRhdGE6IHBhcnNlID0gVCwgZm9yIEZMRVg6IGZsZXggPSBUCmNybSRhZGREZXRhaWxlZE1ldHJpY3Mobi5jb3JlcyA9IDMwKQpgYGAKCmdldCB0b3RhbCBudW1iZXIgb2Ygc2VxdWVuY2VkIG51Y2xlaQpgYGB7cn0Kc3VtKHNhcHBseShjcm0kY21zLCBmdW5jdGlvbihkKSBkaW0oZClbMl0pKQpgYGAKCnBsb3QgVU1JcyBhbmQgZ2VuZXMgcGVyIHNhbXBsZSBiZWZvcmUgZmlsdGVyaW5nLCBob3Jpem9udGFsIGxpbmVzIGluZGljYXRlIHRoZSBtZWRpYW4gdmFsdWVzIGZvciBhbGwgc2FtcGxlcwpgYGB7ciwgd2FybmluZz1GfQpjcm0kcGxvdERldGFpbGVkTWV0cmljcyhjb21wLmdyb3VwID0gImNvbmRpdGlvbiIsCiAgICAgICAgICAgICAgICAgICAgICAgIG1ldHJpY3MgPSBjKCJVTUlfY291bnQiLCAgImdlbmVfY291bnQiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgIHBsb3QuZ2VvbSA9ICJ2aW9saW4iKQpgYGAKR2VuZXJhdGUgYSBkYXRhIGZyYW1lIHdpdGggdGhlIG1ldHJpY3MuCmBgYHtyfQptZXRyaWNzIDwtIGNybSRzdW1tYXJ5Lm1ldHJpY3MKbWV0cmljcyAlPD4lIGFzLmRhdGEuZnJhbWUoKQptZXRyaWNzCmBgYApHZXQgbWVkaWFuIGdlbmVzIHBlciBjZWxsIGFjcm9zcyBhbGwgc2FtcGxlcyBiZWZvcmUgZmlsdGVyaW5nCihpbiBjYXNlIG9mIFBhcnNlOiAiaGczOF9tZWRpYW5fZ2VuZXNfcGVyX2NlbGwiKS4KYGBge3J9Cm1lZGlhbihtZXRyaWNzW21ldHJpY3MkbWV0cmljPT0ibWVkaWFuIGdlbmVzIHBlciBjZWxsIixdJHZhbHVlKQpgYGAKCkdldCBtZWRpYW4gVU1JcyBwZXIgY2VsbCBhY3Jvc3MgYWxsIHNhbXBsZXMgYmVmb3JlIGZpbHRlcmluZwooaW4gY2FzZSBvZiBQYXJzZTogImhnMzhfbWVkaWFuX3RzY3BfcGVyX2NlbGwiKS4KYGBge3J9Cm1lZGlhbihtZXRyaWNzW21ldHJpY3MkbWV0cmljPT0ibWVkaWFuIHVtaSBjb3VudHMgcGVyIGNlbGwiLF0kdmFsdWUpCmBgYAoKSWYgYW1iaWVudCBSTkEgcmVtb3ZhbCBpcyBwYXJ0IG9mIHRoZSBwaXBlbGluZSwgaXQgaXMgcGVyZm9ybWVkIGhlcmUuIFJlbGV2YW50IGNvZGUgaXMgbG9jYXRlZCBmdXJ0aGVyIGRvd24gaW4gdGhlIHNjcmlwdCB1bmRlciA4LgoKIyA0LiBFbWJlZCBjZWxscyB1c2luZyBjb25vcyAKVG8gcGxvdCB0aGUgY2VsbHMgaW4gYW4gZW1iZWRkaW5nLCB3ZSBuZWVkIHRvIHByZXByb2Nlc3MgdGhlIHJhdyBjb3VudCBtYXRyaWNlcyB3aXRoIHBhZ29kYTIgKHNldXJhdCBpcyBhbHNvIHBvc3NpYmxlKS4KYGBge3J9CmNybSRkb1ByZXByb2Nlc3NpbmcoKSAjbWluLnRyYW5zY3JpcHRzLnBlci5jZWxsID0gMTAwCmBgYAoKQ3JlYXRlIGVtYmVkZGluZyB1c2luZyBjb25vcy4gVGhpcyBmdW5jdGlvbnMgZXhlY3V0ZXMgdGhlIGZ1bmN0aW9ucyBDb25vcyRuZXcoY21zLCBuLmNvcmVzID0gbi5jb3JlcyksIGJ1aWxkR3JhcGgoKSwgZmluZENvbW11bml0aWVzKCksIGVtYmVkR3JhcGgobWV0aG9kID0gIlVNQVAiKSwgd2l0aCBkZWZhdWx0IHNldHRpbmdzLCB3aGljaCBjYW4gYmUgYWRqdXN0ZWQuIApgYGB7cn0KY3JtJGNyZWF0ZUVtYmVkZGluZygpCmBgYAoKUGxvdCB0aGUgZW1iZWRkaW5nLiBUaGlzIGlzIGJlZm9yZSBmaWx0ZXJpbmcsIHNvIHdlIGFyZSBub3QgeWV0IGludGVyZXN0ZWQgaW4gdGhlIGNsdXN0ZXJzLCB0aG9zZSBhcmUgcmVmaW5lZCBpbiB0aGUgbmV4dCBzY3JpcHQuCmBgYHtyfQpjcm0kcGxvdEVtYmVkZGluZygpCmBgYAoKIyA1LiBGaWx0ZXJpbmcgb3V0IGxvdyBxdWFsaXR5IGNlbGxzCiMjIENlbGwgZGVwdGgKUGxvdCBjZWxsIGRlcHRoIGluIHRoZSBlbWJlZGRpbmcgb3IgYXMgaGlzdG9ncmFtcyBwZXIgc2FtcGxlLgpgYGB7cn0KY3JtJHBsb3RFbWJlZGRpbmcoZGVwdGggPSBUUlVFLCAKICAgICAgICAgICAgIGRlcHRoLmN1dG9mZiA9IDEwMDApCmBgYApgYGB7ciwgZmlnLmhlaWdodD02LCB3YXJuaW5nPUZ9CmNybSRwbG90RGVwdGgoKQpgYGAKCklmIHRoZSBkZXB0aCBkaXN0cmlidXRpb24gdmFyaWVzIGJldHdlZW4gc2FtcGxlcywgd2UgY2FuIHVzZSBzYW1wbGUtc3BlY2lmaWMgY3V0b2Zmcy4KYGBge3J9CmRlcHRoX2N1dG9mZl92ZWMgPC0gYygxZTMsIDFlMywgNTAwLCAxZTMsIDFlMywgNTAwLCAxZTMsIDFlMywgMWUzLCAxZTMsIDFlMywgNTAwKSAlPiUgCiAgc2V0TmFtZXMoY3JtJGRldGFpbGVkLm1ldHJpY3Mkc2FtcGxlICU+JSAKICAgICAgICAgICAgIHVuaXF1ZSgpICU+JSAKICAgICAgICAgICAgIHNvcnQoKSkKCmRlcHRoX2N1dG9mZl92ZWMKYGBgCgpQbG90IHNhbXBsZSBzcGVjaWZpYyBjdXRvZmYgaW4gaGlzdG9ncmFtIGFuZCBlbWJlZGRpbmcuCmBgYHtyLCB3YXJuaW5nPUYsIGZpZy5oZWlnaHQ9Nn0KY3JtJHBsb3REZXB0aChjdXRvZmYgPSBkZXB0aF9jdXRvZmZfdmVjKQpgYGAKCmBgYHtyfQpjcm0kcGxvdEVtYmVkZGluZyhkZXB0aCA9IFRSVUUsIAogICAgICAgICAgICAgZGVwdGguY3V0b2ZmID0gZGVwdGhfY3V0b2ZmX3ZlYykKYGBgCgojIyBNaXRvY2hvbmRyaWFsIGdlbmUgZnJhY3Rpb24KSW52ZXN0aWdhdGUgdGhlIG1pdG9jaG9uZHJpYWwgZ2VuZSBmcmFjdGlvbiBpbiB0aGUgY2VsbHMuIFRoZSBleGFtcGxlIGRhdGFzZXQgaGFzIGEgdmVyeSBsb3cgZXhwcmVzc2lvbiBvZiBtaXRvY2hvbmRyaWFsIGdlbmVzLiBVc3VhbCBjdXRvZmYgaXMgNSUgb3IgaGlnaGVyLgpgYGB7cn0KY3JtJHBsb3RFbWJlZGRpbmcobWl0by5mcmFjID0gVFJVRSwgCiAgICAgICAgICAgICBtaXRvLmN1dG9mZiA9IDAuMDEsIAogICAgICAgICAgICAgc3BlY2llcyA9ICJtb3VzZSIpCmBgYAoKV2UgY2FuIHBsb3QgdGhlIGRpc3RyaWJ1dGlvbiBvZiB0aGUgbWl0b2Nob25kcmlhbCBmcmFjdGlvbiBwZXIgc2FtcGxlIGFuZCBhbHNvIGluY2x1ZGUgc2FtcGxlLXdpc2UgY3V0b2ZmcyAobm90IHNob3duIGhlcmUgYXMgdGhlIGV4YW1wbGUgZGF0YXNldCBoYXMgb25seSBhIHZlcnkgbG93IGV4cHJlc3Npb24gb2YgbWl0b2Nob25kcmlhbCBnZW5lcykuCmBgYHtyLCBlY2hvPVRSVUUsIHJlc3VsdHM9J2hpZGUnLCBmaWcuaGVpZ2h0PTYsIHdhcm5pbmc9Rn0KY3JtJHBsb3RNaXRvRnJhY3Rpb24oY3V0b2ZmID0gMC4wMSkKYGBgCgojIyBEb3VibGV0IGRldGVjdGlvbgpEb3VibGV0IGRldGVjdGlvbiB3aXRoaW4gQ1JNZXRyaWNzIGlzIHBvc3NpYmxlIHdpdGggdGhlIHB5dGhvbiBwYWNrYWdlcyBzY3J1YmxldCBvciBEb3VibGV0RGV0ZWN0aW9uClNjcnVibGV0IGlzIHRoZSBkZWZhdWx0IG1ldGhvZCwgd2hpY2ggaXMgZmFzdC4gRG91YmxldERldGVjdGlvbiBpcyBzaWduaWZpY2FudGx5IHNsb3dlciwgYnV0IHBlcmZvcm1zIGJldHRlciBhY2NvcmRpbmcgdG8gdGhpcyByZXZpZXcgKGh0dHBzOi8vd3d3LnNjaWVuY2VkaXJlY3QuY29tL3NjaWVuY2UvYXJ0aWNsZS9waWkvUzI0MDU0NzEyMjAzMDQ1OTIpLiBXaGNpaCBvbmUgdG8gY2hvb3NlIGRlcGVuZHMgb24gdGhlIGRhdGFzZXQgYW5kIHRlY2hub2xvZ3kgdXNlZC4KCkhlcmUgd2UgdXNlIHNjcnVibGV0IGFzIGRlZmF1bHQgbWV0aG9kLgpBIHZpcnR1YWwgZW52aXJvbm1lbnQgaXMgdXNlZCB0byBpbnN0YWxsIHNjcnVibGV0LgpgYGB7cn0KbGlicmFyeShyZXRpY3VsYXRlKQpjb25kYV9jcmVhdGUoInNjcnVibGV0IiwgCiAgICAgICAgICAgICBjb25kYSA9ICIvb3B0L3NvZnR3YXJlL21pbmljb25kYS80LjEyLjAvY29uZGFiaW4vY29uZGEiLCAKICAgICAgICAgICAgIHB5dGhvbl92ZXJzaW9uID0gMy44KQpgYGAKCgpSdW4gc2NydWJsZXQgZnJvbSB3aXRoaW4gUiBvciBnZW5lcmF0ZSBhIFB5dGhvbiBzY3JpcHQgdG8gcnVuIGl0IGZyb20gdGhlIHRlcm1pbmFsLiBUbyBkbyB0aGlzLCBzZXQgZXhwb3J0ID0gVFJVRSBhbmQgcnVuIHB5dGhvbiBzY3J1YmxldC5weSBpbnNpZGUgdGhlIHZpcnR1YWwgZW52aXJvbm1lbnQgdG8gZ2VuZXJhdGUgZGF0YS4KYGBge3J9CmNybSRkZXRlY3REb3VibGV0cyhlbnYgPSAic2NydWJsZXQiLAogICAgICAgICAgICAgICAgICAgY29uZGEucGF0aCA9ICIvb3B0L3NvZnR3YXJlL21pbmljb25kYS80LjEyLjAvY29uZGFiaW4vY29uZGEiLAogICAgICAgICAgICAgICAgICAgbWV0aG9kID0gInNjcnVibGV0IikKYGBgCgpUaGVuLCBsb2FkIGRhdGEgd2l0aDoKYGBge3J9CmNybSRhZGREb3VibGV0cygpCmBgYAoKUGxvdCB0aGUgZXN0aW1hdGVkIGRvdWJsZXRzIGluIHRoZSBlbWJlZGRpbmcuCmBgYHtyfQpjcm0kcGxvdEVtYmVkZGluZyhkb3VibGV0Lm1ldGhvZCA9ICJzY3J1YmxldCIpCmBgYAoKCiMjIFBsb3QgZmlsdGVyZWQgY2VsbHMKUGxvdCBhbGwgY2VsbHMgdG8gYmUgZmlsdGVyZWQgb24gdGhlIGVtYmVkZGluZy4KYGBge3J9CmNybSRwbG90RmlsdGVyZWRDZWxscyh0eXBlID0gImVtYmVkZGluZyIsIAogICAgICAgICAgICAgICAgICAgICAgZGVwdGggPSBUUlVFLCAKICAgICAgICAgICAgICAgICAgICAgIGRlcHRoLmN1dG9mZiA9IDEwMDAsIAogICAgICAgICAgICAgICAgICAgICAgZG91YmxldC5tZXRob2QgPSAic2NydWJsZXQiLCAKICAgICAgICAgICAgICAgICAgICAgIG1pdG8uZnJhYyA9IEZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgIHNwZWNpZXMgPSAibW91c2UiKQpgYGAKClBsb3QgdGhlIGNlbGxzIHRvIGJlIGZpbHRlcmVkIHBlciBzYW1wbGUgaW4gYSBiYXIgcGxvdCB3aGVyZSBjb21iaW5hdGlvbiBtZWFucyBhIGNlbGwgdGhhdCBoYXMgYXQgbGVhc3QgdHdvIGZpbHRlciBsYWJlbHMsIGUuZy4gZG91YmxldCBhbmQgZGVwdGguCmBgYHtyLCB3YXJuaW5nPUZ9CmNybSRwbG90RmlsdGVyZWRDZWxscyh0eXBlID0gImJhciIsIAogICAgICAgICAgICAgICAgICAgICAgZGVwdGggPSBUUlVFLCAKICAgICAgICAgICAgICAgICAgICAgIGRlcHRoLmN1dG9mZiA9IDEwMDAsIAogICAgICAgICAgICAgICAgICAgICAgZG91YmxldC5tZXRob2QgPSAic2NydWJsZXQiLCAKICAgICAgICAgICAgICAgICAgICAgIG1pdG8uZnJhYyA9IEZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgIHNwZWNpZXMgPSAibW91c2UiKQpgYGAKCgojIDYuIEZpbHRlciBhbmQgc2F2ZSBjb3VudCBtYXRyaWNlcwpGaW5hbGx5LCBmaWx0ZXIgdGhlIGNvdW50IG1hdHJpY2VzIHRvIGNyZWF0ZSBhIGNsZWFuZWQgbGlzdCB0byBiZSB1c2VkIGluIGRvd25zdHJlYW0gYXBwbGljYXRpb25zLgpJZiBjZWxsYmVuZGVyIHNob3VsZCBiZSB1c2VkIHRoZW4gY21zIG5lZWRzIHRvIGJlIGFkZGVkIGFmdGVyIGNlbGxiZW5kZXIgYW5kIHRoZW4gYWxsIHN1YnNlcXVlbnQgZmlsdGVyaW5nIGlzIGRvbmUuCmBgYHtyfQpjcm0kZmlsdGVyQ21zKGRlcHRoLmN1dG9mZiA9IDEwMDAsIAogICAgICAgICAgICAgIG1pdG8uY3V0b2ZmID0gTlVMTCwgCiAgICAgICAgICAgICAgZG91YmxldHMgPSAic2NydWJsZXQiLAogICAgICAgICAgICAgIHNhbXBsZXMudG8uZXhjbHVkZSA9IE5VTEwsCiAgICAgICAgICAgICAgc3BlY2llcyA9ICJtb3VzZSIsCiAgICAgICAgICAgICAgdmVyYm9zZSA9IFRSVUUpCmBgYAoKVGhlIGZpbHRlcmVkIGxpc3Qgb2YgY291bnQgbWF0cmljZXMgaXMgc3RvcmVkIGluICRjbXMuZmlsdGVyZWQgd2hpY2ggY2FuIGJlIHNhdmVkIG9uIGRpc2suCmBgYHtyfQpxc2F2ZShjcm0kY21zLmZpbHRlcmVkLCAiY21zX2ZpbHRlcmVkW19jZWxsYmVuZGVyXS5xcyIsIAogICAgICBudGhyZWFkcyA9IDEwKQpgYGAKCiMgNy4gUGxvdCBkZXRhaWxlZCBtZXRyaWNzIGFmdGVyIGZpbHRlcmluZwpgYGB7cn0KY21zIDwtIHFyZWFkKCJjbXNfZmlsdGVyZWRbX2NlbGxiZW5kZXJdLnFzIikKYGBgCgpDcmVhdGUgYSBuZXcgY3JtIG9iamVjdCB3aXRoIGNtcyBhZnRlciBmaWx0ZXJpbmcuCmBgYHtyfQpjcm1fZmlsdGVyZWQgPC0gQ1JNZXRyaWNzJG5ldyhjbXMgPSBjcm0kY21zLmZpbHRlcmVkLCBuLmNvcmVzID0gNjAsIG1ldGFkYXRhPSJwYXRoX3RvX21ldGFkYXRhLmNzdiIpCmNybV9maWx0ZXJlZCRhZGRTdW1tYXJ5RnJvbUNtcygpCmNybV9maWx0ZXJlZCRhZGREZXRhaWxlZE1ldHJpY3Mobi5jb3JlcyA9IDMwKQpgYGAKClRvdGFsIG51bWJlciBvZiBudWNsZWkgcmV0YWluZWQgYWZ0ZXIgUUM6CmBgYHtyfQpzdW0oc2FwcGx5KGNybV9maWx0ZXJlZCRjbXMsIGZ1bmN0aW9uKGQpIGRpbShkKVsyXSkpCmBgYAoKUGxvdCBVTUlzIGFuZCBnZW5lcyBwZXIgc2FtcGxlIGFmdGVyIFFDIGZpbHRlcmluZywgaG9yaXpvbnRhbCBsaW5lcyBpbmRpY2F0ZSB0aGUgbWVkaWFuIHZhbHVlcyBmb3IgYWxsIHNhbXBsZXMuCmBgYHtyfQpjcm1fZmlsdGVyZWQkcGxvdERldGFpbGVkTWV0cmljcyhjb21wLmdyb3VwID0gImNvbmRpdGlvbiIsCiAgICAgICAgICAgICAgICAgICAgICAgIG1ldHJpY3MgPSBjKCJVTUlfY291bnQiLCAgImdlbmVfY291bnQiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgIHBsb3QuZ2VvbSA9ICJ2aW9saW4iKQpgYGAKR2VuZXJhdGUgYSBkYXRhIGZyYW1lIHdpdGggdGhlIG1ldHJpY3MuCmBgYHtyfQptZXRyaWNzIDwtIGNybV9maWx0ZXJlZCRzdW1tYXJ5Lm1ldHJpY3MKbWV0cmljcyAlPD4lIGFzLmRhdGEuZnJhbWUoKQptZXRyaWNzCmBgYApNZWRpYW4gZ2VuZXMgcGVyIGNlbGwgYWNyb3NzIGFsbCBzYW1wbGVzIGFmdGVyIFFDIGZpbHRlcmluZyAoaW4gY2FzZSBvZiBQYXJzZTogImhnMzhfbWVkaWFuX2dlbmVzX3Blcl9jZWxsIikuCmBgYHtyfQptZWRpYW4obWV0cmljc1ttZXRyaWNzJG1ldHJpYz09Im1lZGlhbiBnZW5lcyBwZXIgY2VsbCIsXSR2YWx1ZSkKYGBgCgpNZWRpYW4gVU1JcyBwZXIgY2VsbCBhY3Jvc3MgYWxsIHNhbXBsZXMgYWZ0ZXIgUUMgZmlsdGVyaW5nIChpbiBjYXMgb2YgUGFyc2U6ICJoZzM4X21lZGlhbl90c2NwX3Blcl9jZWxsIikuCmBgYHtyfQptZWRpYW4obWV0cmljc1ttZXRyaWNzJG1ldHJpYz09Im1lZGlhbiB1bWkgY291bnRzIHBlciBjZWxsIixdJHZhbHVlKQpgYGAKCgojIDguIE9wdGlvbmFsOiByZW1vdmUgYW1iaWVudCBSTkEKYGBge3IsIGluY2x1ZGU9Rn0KY3JtIDwtIHFyZWFkKCJjcm1fY2VsbGJlbmRlcl93aXRob3V0X2N0cmwzX2FuZF90cmVhdGVkNS5xcyIsIG50aHJlYWRzPTEwKQpgYGAKCldlIHVzZSB1c3VhbGx5IGNlbGxiZW5kZXIsIFNvdXBYIGlzIGFsc28gaW5jbHVkZWQgaW4gQ1JNZXRyaWNzLgpQZXJmb3JtIHRoaXMgc3RlcCBiZWZvcmUgZmlsdGVyaW5nLCBpdCBjaGFuZ2VzIHRoZSBudW1iZXIgb2YgY2FsbGVkIGNlbGxzIGFuZCB0aGVpciBnZW5lIGNvdW50cy4KCkluIHRoZSBvbGQgY2VsbGJlbmRlciB2ZXJzaW9uIChiZWZvcmUgMC4zLjApIHdlIG5lZWRlZCB0byBzcGVjaWZ5IGV4cGVjdGVkIG51bWJlciBvZiBjZWxscyBhbmQgdG90YWwgZHJvcGxldHMgaW5jbHVkZWQuIEFzIGhpbnRlZCBpbiB0aGUgbWFudWFsLCB0aGUgbnVtYmVyIG9mIHRvdGFsIGRyb3BsZXRzIGluY2x1ZGVkIGNvdWxkIGJlIGV4cGVjdGVkIG51bWJlciBvZiBjZWxscyBtdWx0aXBsaWVkIGJ5IDMsIHRoaXMgY2FuIHN0aWxsIGJlIHBsb3R0ZWQgaGVyZSB3aGVuIHNldHRpbmcgc2hvdy50b3RhbC5kcm9wbGV0cz1ULgpBZGRpdGlvbmFsbHksIGluIGNhc2Ugb2YgUGFyc2UsIHRoZSBmb2xsb3dpbmcgZnVuY3Rpb24gY3JlYXRlcyB0aGUgaW5wdXQgZmlsZXMgZm9yIGNlbGxiZW5kZXIgZnJvbSB0aGUgUGFyc2Ugb3V0cHV0LiAKYGBge3IsIGZpZy5oZWlnaHQ9Niwgd2FybmluZz1GfQpjcm0kcHJlcGFyZUNlbGxiZW5kZXIoc2hvdy5leHBlY3RlZC5jZWxscyA9IFQsIHNob3cudG90YWwuZHJvcGxldHMgPSBUKQpgYGAKCgpUaGUgZm9sbG93aW5nIGZ1bmN0aW9uIHByZXBhcmVzIGFuZCBzYXZlcyB0aGUgY2VsbGJlbmRlciBzY3JpcHQuIFRvIG9ubHkgc2VsZWN0IHNwZWNpZmljIHNhbXBsZXMsIHVzZSB0aGUgc2FtcGxlcyBwYXJhbWV0ZXIuIAp0b3RhbC5kcm9wbGV0cyBhbmQgZXhwZWN0ZWQuY2VsbHMgaXMgYSBsb2dpY2FsIGJlY2F1c2UgaW4gY2VsbGJlbmRlciBmcm9tIHYwLjMuMCB0b3RhbC5kcm9wbGV0cyBhbmQgZXhwZWN0ZWQuY2VsbHMgZG8gbm90IG5lZWQgdG8gYmUgc3BlY2lmaWVkIGFueW1vcmUuIDxicj4KCQnil4sgRkFMU0UgaXMgZGVmYXVsdCA8YnI+CgkJ4peLIFRSVUUgd291bGQgdGFrZSBleHBlY3RlZC5jZWxscyBmcm9tIG1ldGFkYXRhIGFuZCB0b3RhbCBkcm9wbGV0cyB3b3VsZCBiZSAzeCBleHBlY3RlZCBjZWxscyA8YnI+CgkJ4peLIGFsdGVybmF0aXZlbHkgZ2l2ZSBvd24gbnVtYmVycyAobmFtZWQgdmVjdG9yKSAKYGBge3J9CmNybSRzYXZlQ2VsbGJlbmRlclNjcmlwdChmaWxlID0gImNlbGxiZW5kZXJfc2NyaXB0LnNoIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICBmcHIgPSAwLjAxLCAKICAgICAgICAgICAgICAgICAgICAgICAgIGVwb2NocyA9IDE1MCwgCiAgICAgICAgICAgICAgICAgICAgICAgICB1c2UuZ3B1ID0gVFJVRSwgdG90YWwuZHJvcGxldHMgPSBGLCBleHBlY3RlZC5jZWxscyA9IEYpICMgY291bGQgYmUgdG90YWwuZHJvcGxldHMgPSBkcm9wbGV0cyAobmFtZWQgdmVjdG9yKQpgYGAKCldlIGNhbiBydW4gdGhpcyBzY3JpcHQgaW4gdGhlIHRlcm1pbmFsIChzY3JlZW4gc2Vzc2lvbikuIEhlcmUsIHdlIGFjdGl2YXRlIHRoZSBlbnZpcm9ubWVudDogY29uZGEgYWN0aXZhdGUgY2VsbGJlbmRlcl92MC4zLjAgYW5kIHdlIHJ1biB0aGUgYmFzaCBzY3JpcHQ6IHNoIGNlbGxiZW5kZXJfc2NyaXB0LnNoLgoKIyMgUGxvdCBjZWxsYmVuZGVyIHJlc3VsdHMKUGxvdCB0aGUgY2hhbmdlcyBpbiBjZWxsIG51bWJlcnMgZm9sbG93aW5nIENlbGxCZW5kZXIgZXN0aW1hdGlvbnMuCmBgYHtyLCB3YXJuaW5nPUZ9CmNybSRwbG90Q2JDZWxscygpCmBgYApgYGB7ciwgZXZhbD1GLCBpbmNsdWRlPUZ9CmRldnRvb2xzOjpsb2FkX2FsbCgiL3Blb3BsZS9ucnEzNjQvQ1JNZXRyaWNzX0xXIikKY3JtICU8PiUgQ1JNZXRyaWNzJG5ldygpCmBgYAoKUGxvdCB0aGUgQ2VsbEJlbmRlciB0cmFpbmluZyByZXN1bHRzLgpgYGB7ciwgZmlnLmhlaWdodD02LCB3YXJuaW5nPUZ9CmNybSRwbG90Q2JUcmFpbmluZygpCmBgYAoKUGxvdCB0aGUgY2VsbCBwcm9iYWJpbGl0aWVzLgpgYGB7ciwgZmlnLmhlaWdodD02LCB3YXJuaW5nPUZ9CmNybSRwbG90Q2JDZWxsUHJvYnMoKQpgYGAKClBsb3QgdGhlIGlkZW50aWZpZWQgYW1iaWVudCBnZW5lcyBwZXIgc2FtcGxlLgpgYGB7ciwgZmlnLmhlaWdodD02fQpjcm0kcGxvdENiQW1iRXhwKGN1dG9mZiA9IDAuMDA1KQpgYGAKCkxhc3RseSwgcGxvdCB0aGUgcHJvcG9ydGlvbiBvZiBzYW1wbGVzIGV4cHJlc3NpbmcgYW1iaWVudCBnZW5lcy4gTUFMQVQxIGlzIGlkZW50aWZpZWQgYXMgYW4gYW1iaWVudCBnZW5lIGluIGFsbW9zdCBhbGwgc2FtcGxlcyB3aGljaCBpcyBleHBlY3RlZC4KYGBge3IsIHdhcm5pbmc9Rn0KY3JtJHBsb3RDYkFtYkdlbmVzKGN1dG9mZiA9IDAuMDA1KQpgYGAKCiMjIEFkZCBjZWxsYmVuZGVyIGZpbHRlcmVkIGNtcyAKVGhlbiwgd2UgY2FuIGFkZCB0aGUgY2VsbGJlbmRlciBmaWx0ZXJlZCBjbXMgdG8gb3VyIG9iamVjdC4gQWRkaXRpb25hbGx5LCBpdCBpcyByZWNvbW1lbmRlZCB0byBnZW5lcmF0ZSBuZXcgc3VtbWFyeSBtZXRyaWNzIGZyb20gdGhlIGFkanVzdGVkIGNtcyB3aGljaCBjYW4gdGhlbiBiZSBwbG90dGVkLgpgYGB7cn0KY3JtJGNtcyA8LSBOVUxMCmNybSRhZGRDbXMoY2VsbGJlbmRlciA9IFQpCmNybSRhZGRTdW1tYXJ5RnJvbUNtcygpCmNybSRkZXRhaWxlZC5tZXRyaWNzIDwtIE5VTEwKY3JtJGFkZERldGFpbGVkTWV0cmljcygpCnFzYXZlKGNybSwgImNybV9jZWxsYmVuZGVyLnFzIikKYGBgCg==